package com.suntianci.stock.service.Impl;

import com.google.common.collect.Lists;
import com.google.gson.Gson;
import com.suntianci.stock.constant.ParseType;
import com.suntianci.stock.face.StockCacheFace;
import com.suntianci.stock.mapper.*;
import com.suntianci.stock.pojo.entity.StockBlockRtInfo;
import com.suntianci.stock.pojo.entity.StockMarketIndexInfo;
import com.suntianci.stock.pojo.entity.StockOuterMarketIndexInfo;
import com.suntianci.stock.pojo.entity.StockRtInfo;
import com.suntianci.stock.pojo.vo.StockInfoConfig;
import com.suntianci.stock.service.StockTimerTaskService;
import com.suntianci.stock.utils.DateTimeUtil;
import com.suntianci.stock.utils.IdWorker;
import com.suntianci.stock.utils.ParserStockInfoUtil;
import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import javax.annotation.PostConstruct;
import java.math.BigDecimal;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * @ClassName StockTimerTaskServiceImpl
 * @Author 孙天赐
 * @Date 2025/1/4 16:28
 * @Description TODO: 采集股票数据的定时任务的服务实现
 */
@Service
@Slf4j
public class StockTimerTaskServiceImpl implements StockTimerTaskService {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private StockInfoConfig stockInfoConfig;

    @Autowired
    private IdWorker idWorker;

    @Autowired
    private StockBusinessMapper stockBusinessMapper;

    @Autowired
    private StockMarketIndexInfoMapper stockMarketIndexInfoMapper;

    @Autowired
    private ParserStockInfoUtil parserStockInfoUtil;

    @Autowired
    private StockRtInfoMapper stockRtInfoMapper;

    @Autowired
    private StockBlockRtInfoMapper stockBlockRtInfoMapper;

    @Autowired
    private StockOuterMarketIndexInfoMapper stockOuterMarketIndexInfoMapper;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;

    @Autowired
    private StockCacheFace stockCacheFace;

    /**
     * 必须保证该对象无状态，即只能有防盗链和用户客户端标识（也就是在initData方法中设置的参数），不能有别的东西
     */
    private HttpEntity<Object> httpEntity;

    /**
     * bean生命周期初始化回调方法
     */
    @PostConstruct
    public void initData() {
        // 设置请求头，添加防盗链和用户标识。如果不设置，会被接口拒绝访问
        HttpHeaders headers = new HttpHeaders();
        // 防盗链
        headers.add("Referer", "https://finance.sina.com.cn/stock/");
        // 用户客户端标识
        headers.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36");
        // 维护http请求实体对象
        httpEntity = new HttpEntity<>(headers);
    }

    /**
     * 获取国内大盘的实时数据信息
     */
    @Override
    public void getInnerMarketInfo() {
        // 1.采集原始数据
        // 1.1 组装url地址      join方法：按照逗号分割，将集合stockInfoConfig.getInner()中的数据组装成新的字符串
        String url = stockInfoConfig.getMarketUrl() + String.join(",", stockInfoConfig.getInner());
        // 1.2 使用RestTemplate发起请求 参数一：地址   参数二：请求方式   参数三：请求实体对象   参数四：返回值类型
        ResponseEntity<String> responseEntities = restTemplate.exchange(url, HttpMethod.GET, httpEntity, String.class);
        int statusCodeValue = responseEntities.getStatusCodeValue();
        if (statusCodeValue != 200) {
            // 请求失败
            log.error("当前时间点{}，采集大盘数据失败，http状态码：{}", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), statusCodeValue);
            return;
        }
        // 1.3 获取js格式的数据
        String jsData = responseEntities.getBody();
        log.info("大盘数据采集完成！");

        // 2.书写正则表达式
        // 2.1 正则表达式，用于捕获组
        String reg = "var hq_str_(.+)=\"(.+)\";";
        // 2.2 表达式编译
        Pattern pattern = Pattern.compile(reg);
        // 2.3 匹配字符串
        Matcher matcher = pattern.matcher(jsData);
        // 3、4.逐行解析并封装数据到实体类中
        List<StockMarketIndexInfo> marketEntities = new ArrayList<>();
        while (matcher.find()) {
            // 3.使用正则表达式解析数据
            // 3.1 获取大盘编码（第一组）
            String marketCode = matcher.group(1);
            // 3.2 获取其他信息（第二组）
            String otherInfo = matcher.group(2);
            // 3.3 将other以逗号切割，获取大盘的详情信息
            String[] allData = otherInfo.split(",");
            // 3.4 获取到全部数据
            // 获取大盘名称
            String marketName = allData[0];
            // 获取开盘点
            BigDecimal openPoint = new BigDecimal(allData[1]);
            // 获取前收盘点
            BigDecimal preClosePoint = new BigDecimal(allData[2]);
            // 获取当前点
            BigDecimal curPoint = new BigDecimal(allData[3]);
            // 获取最高点
            BigDecimal maxPoint = new BigDecimal(allData[4]);
            // 获取最低点
            BigDecimal minPoint = new BigDecimal(allData[5]);
            // 获取成交量
            Long tradeAmt = new Long(allData[8]);
            // 获取成交金额
            BigDecimal tradeVol = new BigDecimal(allData[9]);
            // 获取当前日期时间
            Date curTime = DateTimeUtil.getDateTimeWithoutSecond(allData[30] + " " + allData[31]).toDate();
            // 4.将数据封装到实体对象中
            StockMarketIndexInfo marketEntity = StockMarketIndexInfo.builder()
                    .id(idWorker.nextId())
                    .marketCode(marketCode)
                    .marketName(marketName)
                    .openPoint(openPoint)
                    .preClosePoint(preClosePoint)
                    .curPoint(curPoint)
                    .maxPoint(maxPoint)
                    .minPoint(minPoint)
                    .tradeAmount(tradeAmt)
                    .tradeVolume(tradeVol)
                    .curTime(curTime)
                    .build();
            // 收集到List中，后续用于批量添加到数据库中
            marketEntities.add(marketEntity);
        }
        log.info("大盘数据解析完成！");
        // 4.调用mybatis入库
        int row = stockMarketIndexInfoMapper.batchInsert(marketEntities);
        if (row > 0) {
            // 5.数据采集完毕后，通过RabbitMQ通知backend工程刷新缓存
            // 发送日期对象，接收方通过接收的日期与当前日期对比，能判断出数据延迟的时长，用于运维通知处理
            rabbitTemplate.convertAndSend("stockExchange", "inner.market", new Date());
            log.info("当前时间{}，插入了大盘数据{}成功！", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), marketEntities);
        } else {
            log.error("当前时间{}，插入了大盘数据{}失败！", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), marketEntities);
        }
    }

    /**
     * 获取股票的实时数据信息
     */
    @Override
    public void getStockRtIndexInfo() {
        // 后面的代码使用缓存层获取数据，这里的获取数据不再需要
//        // 1. 获取并处理全部股票编码
//        // 1.1 获取全部股票编码（没有前缀）
//        List<String> allStockCodes = stockBusinessMapper.getAllStockCode();
//        // 1.2 使用stream流添加前缀
//        List<String> allRightStockCodes = allStockCodes.stream().map(code -> {
//            if (code.startsWith("0")) {
//                code = "sz" + code;
//            } else {
//                code = "sh" + code;
//            }
//            return code;
//        }).collect(Collectors.toList());
        // 从缓存层获取数据
        List<String> allRightStockCodes = stockCacheFace.getAllStockCodeWithPredix();
        // 1.3 使用guava工具类切分集合，避免单次采集数据量过大
        List<List<String>> partitionStockCodes = Lists.partition(allRightStockCodes, 10);

        // 2、3、4. 采集数据、处理数据、数据ruk
        for (List<String> StockCodes : partitionStockCodes) {
            // 方案一，原始方案，单线程串行采集，效率不高
//            // 2. 根据股票编码采集得到原始数据
//            // 2.1 组装url地址
//            String url = stockInfoConfig.getMarketUrl() + String.join(",", StockCodes);
//            // 2.2 发送请求，采集数据  参数一：地址   参数二：请求方式   参数三：请求实体对象   参数四：返回值类型
//            ResponseEntity<String> responseEntities = restTemplate.exchange(url, HttpMethod.GET, httpEntity, String.class);
//            int statusCodeValue = responseEntities.getStatusCodeValue();
//            if (statusCodeValue != 200) {
//                // 请求失败
//                log.error("当前时间点{}，采集数据失败，http状态码：{}", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), statusCodeValue);
//                return;
//            }
//            // 2.3 获取js格式的原始数据
//            String jsData = responseEntities.getBody();
//            // 3. 使用工具类，解析并处理采集得到的数据
//            List<StockRtInfo> list = parserStockInfoUtil.parser4StockOrMarketInfo(jsData, ParseType.ASHARE);
//            // 4. 批量入库
//            int row = stockRtInfoMapper.batchInsert(list);
//            if (row > 0) {
//                log.info("当前时间{}，插入了股票数据{}成功！", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), list);
//            } else {
//                log.error("当前时间{}，插入了股票数据{}失败！", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), list);
//            }


            // 方案二，多线程采集
            // 问题：1.每次来任务，就创建一个线程，复用性差
            //       2. 如果多线程使用不当，会造成CPU竞争激烈，导致频繁的上下文切换，导致程序性能降低
//            new Thread(()->{
//                // 2. 根据股票编码采集得到原始数据
//                // 2.1 组装url地址
//                String url = stockInfoConfig.getMarketUrl() + String.join(",", StockCodes);
//                // 2.2 发送请求，采集数据  参数一：地址   参数二：请求方式   参数三：请求实体对象   参数四：返回值类型
//                ResponseEntity<String> responseEntities = restTemplate.exchange(url, HttpMethod.GET, httpEntity, String.class);
//                int statusCodeValue = responseEntities.getStatusCodeValue();
//                if (statusCodeValue != 200) {
//                    // 请求失败
//                    log.error("当前时间点{}，采集数据失败，http状态码：{}", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), statusCodeValue);
//                    return;
//                }
//                // 2.3 获取js格式的原始数据
//                String jsData = responseEntities.getBody();
//                // 3. 使用工具类，解析并处理采集得到的数据
//                List<StockRtInfo> list = parserStockInfoUtil.parser4StockOrMarketInfo(jsData, ParseType.ASHARE);
//                // 4. 批量入库
//                int row = stockRtInfoMapper.batchInsert(list);
//                if (row > 0) {
//                    log.info("当前时间{}，插入了股票数据{}成功！", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), list);
//                } else {
//                    log.error("当前时间{}，插入了股票数据{}失败！", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), list);
//                }
//            }).start();

            // 方案三：引入线程池，解决方案二的缺陷
            threadPoolTaskExecutor.execute(() -> {
                // 2. 根据股票编码采集得到原始数据
                // 2.1 组装url地址
                String url = stockInfoConfig.getMarketUrl() + String.join(",", StockCodes);
                // 2.2 发送请求，采集数据  参数一：地址   参数二：请求方式   参数三：请求实体对象   参数四：返回值类型
                ResponseEntity<String> responseEntities = restTemplate.exchange(url, HttpMethod.GET, httpEntity, String.class);
                int statusCodeValue = responseEntities.getStatusCodeValue();
                if (statusCodeValue != 200) {
                    // 请求失败
                    log.error("当前时间点{}，采集数据失败，http状态码：{}", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), statusCodeValue);
                    return;
                }
                // 2.3 获取js格式的原始数据
                String jsData = responseEntities.getBody();
                // 3. 使用工具类，解析并处理采集得到的数据
                List<StockRtInfo> list = parserStockInfoUtil.parser4StockOrMarketInfo(jsData, ParseType.ASHARE);
                // 4. 批量入库
                int row = stockRtInfoMapper.batchInsert(list);
                if (row > 0) {
                    log.info("当前时间{}，插入了股票数据{}成功！", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), list);
                } else {
                    log.error("当前时间{}，插入了股票数据{}失败！", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), list);
                }
            });

        }
    }

    /**
     * 获取板块的实时数据信息
     */
    @Override
    public void getStockBlockRtIndexInfo() {
        // 1. 获取原始数据
        String originData = restTemplate.getForObject(stockInfoConfig.getBlockUrl(), String.class);
        // 2. 处理数据，提取信息，封装到实体类中
        List<StockBlockRtInfo> stockBlockRtInfos = parserStockInfoUtil.parse4StockBlock(originData);
        // 3.切分数据，多线程分片插入数据库中
        List<List<StockBlockRtInfo>> partition = Lists.partition(stockBlockRtInfos, 10);
        for (List<StockBlockRtInfo> blockRtInfo : partition) {
            threadPoolTaskExecutor.execute(() -> {
                // 向数据库中批量插入数据
                int row = stockBlockRtInfoMapper.batchInsert(blockRtInfo);
                if (row > 0) {
                    log.info("当前时间{}，插入了板块数据{}成功！", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), stockBlockRtInfos);
                } else {
                    log.error("当前时间{}，插入了板块数据{}失败！", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), stockBlockRtInfos);
                }
            });
        }

    }

    /**
     * 获取国外大盘的信息
     */
    @Override
    public void getOuterMarketInfo() {
        // 组装url地址
        String url = stockInfoConfig.getMarketUrl() + String.join(",", stockInfoConfig.getOuter());
        // 使用RestTemplate发起请求 参数一：地址   参数二：请求方式   参数三：请求实体对象   参数四：返回值类型
        ResponseEntity<String> entities = restTemplate.exchange(url, HttpMethod.GET, httpEntity, String.class);
        if (entities.getStatusCodeValue() != 200) {
            log.error("当前时间点{}，采集外盘数据失败，http状态码：{}", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), entities.getStatusCodeValue());
        }
        // 获取json格式的数据
        String jsData = entities.getBody();
        // 解析数据
        List<StockOuterMarketIndexInfo> list = parserStockInfoUtil.parser4StockOrMarketInfo(jsData, ParseType.OUTER);
        log.info("国外大盘数据解析完成！");
        // 将数据批量写入数据库中
        int row = stockOuterMarketIndexInfoMapper.batchInsert(list);
        if (row > 0) {
            log.info("当前时间{}，插入了外盘数据{}成功！", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), list);
        } else {
            log.error("当前时间{}，插入了外盘数据{}失败！", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), list);
        }

    }


}
